package ro.toporjinschi.androidorm;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class EntityManager {

    private static final String PARAMETER = "= ?";
    private static final String TABLE_NAME = "{tableName}";
    private static final String ID_NAME = "{idName}";
    private static final String SELECT_STATEMENT = "SELECT * FROM " + TABLE_NAME;
    private static final String SELECT_STATEMENT_ID = "SELECT * FROM " + TABLE_NAME + " WHERE " + ID_NAME + PARAMETER;
    private static EntityManager instance;
    private final SQLiteOpenHelper dbHelper;
    private final Map<String, EntityMetadata> entityMappings = new HashMap<String, EntityMetadata>();
    private SQLiteDatabase db;

    private EntityManager() {
        this.dbHelper = ORMLayer.getInstance().getSqLiteOpenHelper();
    }

    public static EntityManager getInstance() {
        if (instance == null) {
            instance = new EntityManager();
        }
        return instance;
    }

    public void beginTransaction() {
        initWritableDb();
        this.db.beginTransaction();
    }

    public void endTransaction() {
        if (this.db != null && this.db.isOpen() && this.db.inTransaction()) {
            this.db.setTransactionSuccessful();
            this.db.endTransaction();
        }
        if (this.db != null && this.db.isOpen()) {
            this.db.close();
        }
    }

    public EntityMetadata getEntityMaping(Entity entity) {
        return getEntityMaping(entity.getClass());
    }

    public EntityMetadata getEntityMaping(Class<? extends Entity> clazz) {
        final String className = clazz.getName();
        EntityMetadata em = this.entityMappings.get(className);
        if (em == null) {
            this.entityMappings.put(className, EntityMetadata.build(clazz));
            em = this.entityMappings.get(className);
        }
        return em;
    }

    public <T extends Entity> List<T> get(Class<T> clazz, int idValue) {
        initReadableDb();

        final EntityMetadata em = getEntityMaping(clazz);
        
        String sql = SELECT_STATEMENT_ID.replace(TABLE_NAME, em.getTableName());
        sql = sql.replace(ID_NAME,  em.getIdDbName());//.replacesTemplate(SELECT_STATEMENT_ID, ID_NAME, em.getIdDbName());
        final Cursor cursor = this.db.rawQuery(sql, new String[] { Integer.toString(idValue) });
        return getResult(cursor, clazz, em);
    }

    @SuppressWarnings("unchecked")
    private <T extends Entity> List<T> getResult(Cursor cursor, Class<T> clazz, EntityMetadata em) {
        final List<T> results = new ArrayList<T>();
        while (cursor.moveToNext()) {
            try {
                final Entity entInstance = clazz.newInstance();
                final String[] colSelected = cursor.getColumnNames();
                for (int i = 0; i < colSelected.length; i++) {
                    final FieldMetaData field = em.getFieldMetaData(colSelected[i]);
                    setFieldValue(entInstance, field, cursor);
                }
                results.add((T) entInstance);
            } catch (final IllegalArgumentException e) {
                throw new RuntimeException(e);
            } catch (final IllegalAccessException e) {
                throw new RuntimeException(e);
            } catch (final InstantiationException e) {
                throw new RuntimeException(e);
            }
        }
        return results;
    }

    @SuppressWarnings("unchecked")
    public <T extends Entity> List<T> get(Class<T> clazz) {
        initReadableDb();
        final List<T> results = new ArrayList<T>();
        final EntityMetadata em = getEntityMaping(clazz);
        final String sql = SELECT_STATEMENT.replace(TABLE_NAME, em.getTableName());//.replacesTemplate(SELECT_STATEMENT, TABLE_NAME, em.getTableName());
        final Cursor cursor = this.db.rawQuery(sql, null);
        while (cursor.moveToNext()) {
            try {
                final Entity entInstance = clazz.newInstance();
                final String[] colSelected = cursor.getColumnNames();
                for (int i = 0; i < colSelected.length; i++) {
                    final FieldMetaData field = em.getFieldMetaData(colSelected[i]);
                    setFieldValue(entInstance, field, cursor);
                }
                results.add((T) entInstance);
            } catch (final IllegalArgumentException e) {
                throw new RuntimeException(e);
            } catch (final IllegalAccessException e) {
                throw new RuntimeException(e);
            } catch (final InstantiationException e) {
                throw new RuntimeException(e);
            }
        }
        return results;
    }

    public void insert(Entity instance) {
        initWritableDb();
        final EntityMetadata em = getEntityMaping(instance);
        final ContentValues contentValues = createContentValues(em, instance);
        final long idValue = this.db.insert(em.getTableName(), null, contentValues);
        if (idValue == -1) {
            throw new RuntimeException("Error ocurred when try to insert into table " + em.getTableName());
        }
        try {

            em.getIdField().getSetMethod().invoke(instance, idValue);
        } catch (final IllegalArgumentException e) {
            e.printStackTrace();
        } catch (final IllegalAccessException e) {
            e.printStackTrace();
        } catch (final InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    public void update(Entity entityInstance) {
        initWritableDb();
        final EntityMetadata em = getEntityMaping(entityInstance);
        final ContentValues contentValues = createContentValues(em, entityInstance);
        Integer id = null;
        try {
            id = (Integer) em.getIdValue(entityInstance);
        } catch (final IllegalArgumentException e) {
            e.printStackTrace();
        }
        if (id == null) {
            throw new RuntimeException("ID property is null you cannot update a record based on the null id.");
        }
        this.db.update(em.getTableName(), contentValues, em.getIdDbName() + PARAMETER, new String[] { Integer.toString(id) });
    }

    public <T extends Entity> void delete(Class<T> clazz, int id) {
        initWritableDb();
        final EntityMetadata em = getEntityMaping(clazz);
        this.db.delete(em.getTableName(), em.getIdDbName() + PARAMETER, new String[] { Integer.toString(id) });
    }

    public void delete(Entity entityInstance) {
        initWritableDb();
        final EntityMetadata em = getEntityMaping(entityInstance);
        Integer id = null;
        try {
            id = (Integer) em.getIdValue(entityInstance);
        } catch (final IllegalArgumentException e) {
            e.printStackTrace();
        }
        if (id == null) {
            throw new RuntimeException("ID property is null you cannot delete a record based on the null id.");
        }
        this.db.delete(em.getTableName(), em.getIdDbName() + PARAMETER, new String[] { Integer.toString(id) });
    }

    public <T extends Entity> Query<T> query(Class<T> clazz, String namedQuery) {
        final EntityMetadata em = getEntityMaping(clazz);
        final String query = em.getQuery(namedQuery);
        if (em != null && query != null) {
            return new Query<T>(em, query, this.db);
        }
        return null;
    }

    private void initWritableDb() {
        if (this.db == null || !this.db.isOpen()) {
            this.db = this.dbHelper.getWritableDatabase();
        }
    }

    private void initReadableDb() {
        if (this.db == null || !this.db.isOpen()) {
            this.db = this.dbHelper.getReadableDatabase();
        }
    }

    private ContentValues createContentValues(EntityMetadata em, Entity entityInstance) {
        final ContentValues cv = new ContentValues();
        final List<FieldMetaData> fieldsMD = em.getFields();
        for (int i = 0; i < fieldsMD.size(); i++) {
            final FieldMetaData fmd = fieldsMD.get(i);
            if (fmd.isId()) {
                continue;
            }
            final Field field = fmd.getField();
            final Object value = getFieldValue(entityInstance, fmd);
            cvSetValue(cv, fmd.getDbName(), field, value);
        }
        return cv;
    }

    private void cvSetValue(ContentValues cv, String dbField, Field field, Object value) {
        if (value == null) {
            cv.putNull(dbField);
        } else if (field.getType() == String.class) {
            cv.put(dbField, (String) value);
        } else if (field.getType() == Integer.class) {
            cv.put(dbField, (Integer) value);
        } else if (field.getType() == Byte.class) {
            cv.put(dbField, (Byte) value);
        } else if (field.getType() == Double.class) {
            cv.put(dbField, (Double) value);
        } else if (field.getType() == Float.class) {
            cv.put(dbField, (Float) value);
        } else if (field.getType() == Long.class) {
            cv.put(dbField, (Long) value);
        } else if (field.getType() == Short.class) {
            cv.put(dbField, (Short) value);
        } else if (field.getType() == Boolean.class) {
            cv.put(dbField, (Boolean) value);
        }
    }

    private Object getFieldValue(Entity entityInstance, FieldMetaData fmd) {
        try {
            return fmd.getGetMethod().invoke(entityInstance);
        } catch (final IllegalArgumentException e) {
            throw new RuntimeException(e);
        } catch (final IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (final InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    private void setFieldValue(Entity entInstance, FieldMetaData field, Cursor cursor) {
        final String fieldDbName = field.getDbName();
        try {
            Object value = null;
            if (field.getType() == String.class) {
                value = cursor.getString(cursor.getColumnIndex(fieldDbName));
            } else if (field.getType() == Integer.class) {
                value = cursor.getInt(cursor.getColumnIndex(fieldDbName));
            } else if (field.getType() == Boolean.class) {
                value = cursor.getInt(cursor.getColumnIndex(fieldDbName)) != 0;
            } else if (field.getType() == Float.class) {
                value = cursor.getFloat(cursor.getColumnIndex(fieldDbName));
            } else if (field.getType() == Short.class) {
                value = cursor.getShort(cursor.getColumnIndex(fieldDbName));
            } else if (field.getType() == Long.class) {
                value = cursor.getLong(cursor.getColumnIndex(fieldDbName));
            } else if (field.getType() == Double.class) {
                value = cursor.getDouble(cursor.getColumnIndex(fieldDbName));
            }
            field.getSetMethod().invoke(entInstance, value);
        } catch (final IllegalArgumentException e) {
            e.printStackTrace();
        } catch (final IllegalAccessException e) {
            e.printStackTrace();
        } catch (final InvocationTargetException e) {
            e.printStackTrace();
        }
    }

}